package per.hnvcam.searchext.services.impl;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import per.hnvcam.common.SubSetGenerator;
import per.hnvcam.common.impl.ReducedCombinationGeneratorImpl;
import per.hnvcam.fuzzy.MembershipFunction;
import per.hnvcam.fuzzy.impl.TriangleMembershipFunctionImpl;
import per.hnvcam.genetic.algorithm.Chromosome;
import per.hnvcam.genetic.algorithm.GeneticAlgorithm;
import per.hnvcam.hedge.algebra.Hedge2AlgebraDefinition;
import per.hnvcam.hedge.algebra.Hedge2Quantifier;
import per.hnvcam.searchext.Constants;
import per.hnvcam.searchext.data.dao.ConditionDAO;
import per.hnvcam.searchext.data.dao.RuleDAO;
import per.hnvcam.searchext.services.SpecialQuery;
import per.hnvcam.searchext.data.dao.WebReferenceDAO;
import per.hnvcam.searchext.data.model.*;
import per.hnvcam.searchext.services.RuleService;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by IntelliJ IDEA.
 * User: Vi Cam
 * Date: 26/12/2011
 * Time: 21:22
 * To change this template use File | Settings | File Templates.
 */

@Service("RuleService")
public class RuleServiceImpl extends CallbackSupportServiceImpl implements RuleService, InitializingBean {

   private static final Logger LOG = Logger.getLogger(RuleServiceImpl.class);
   @Autowired
   @Qualifier("SpecialQuery")
   private SpecialQuery specialQuery;

   @Autowired
   @Qualifier("WebReferenceDAO")
   private WebReferenceDAO webReferenceDAO;

   @Autowired
   @Qualifier("RuleDAO")
   private RuleDAO ruleDAO;

   @Autowired
   @Qualifier("ConditionDAO")
   private ConditionDAO conditionDAO;

   @Autowired
   @Qualifier("sessionFactory")
   private SessionFactory sessionFactory;

   @Autowired
   @Qualifier("Hedge2AlgebraDefinition")
   private Hedge2AlgebraDefinition hedge2AlgebraDefinition;

   @Autowired
   @Qualifier("Hedge2Quantifier")
   private Hedge2Quantifier hedge2Quantifier;

   @Autowired
   @Qualifier("GeneticAlgorithm")
   private GeneticAlgorithm geneticAlgorithm;

   private Map<RuleData, Double> supportDegrees;
   private Map<RuleData, Double> confidenceDegrees;
   private List<RuleData> ruleSet;
   private List<RuleData> allBaseRulesData;
   private SystemConfiguration systemConfiguration;
   private Map<String, List<RuleData>> initialRuleSet;
   private int initialRuleSetSize;
   private Map<String, MembershipFunction> cachedMembershipFunctions;
   private double leftSideAdjustment;
   private double rightSideAdjustment;
   private Map<String, Runnable> systemChangedActions;

   public RuleServiceImpl() {
      supportDegrees = new ConcurrentHashMap<RuleData, Double>();
      confidenceDegrees = new ConcurrentHashMap<RuleData, Double>();
      ruleSet = new LinkedList<RuleData>();
      initialRuleSet = new ConcurrentHashMap<String, List<RuleData>>();
      cachedMembershipFunctions = new ConcurrentHashMap<String, MembershipFunction>();
   }

   @Override
   public List<RuleData> getAllBaseRulesData() {
      @SuppressWarnings("rawtypes")
      List orgBaseRules = ruleDAO.findAllBaseRules();

      return getRuleData(orgBaseRules);
   }

   private List<RuleData> getRuleData(List orgRules) {
      List<RuleData> result = new ArrayList<RuleData>();

      for (Object obj : orgRules) {
         Rule rule = (Rule) obj;
         RuleData ruleData = new RuleData();
         ruleData.setClassifiedClass(rule.getClassifiedClass());

         @SuppressWarnings("rawtypes")
         List conditions = conditionDAO.findAllByRuleId(rule.getId());

         for (Object obj2 : conditions) {
            Condition condition = (Condition) obj2;
            ConditionData conditionData = new ConditionData();
            conditionData.setFuzzyAmount(condition.getFuzzyAmount());
            conditionData.setWord(condition.getWord());
            ruleData.getConditions().add(conditionData);
         }
         result.add(ruleData);
      }
      return result;
   }

   @Override
   public List<RuleData> getAllManualRulesData() {
      List orgManualRules = ruleDAO.findAllManualRules();

      return getRuleData(orgManualRules);
   }

   @Override
   public void generateBaseRules() {
      Session session = sessionFactory.openSession();
      Transaction tx = null;
      try {
         tx = session.beginTransaction();
         SystemConfiguration systemConfiguration = specialQuery.getSystemConfiguration();
         Map<Long, List<ConditionData>> trainingData = specialQuery.findAllTrainingData();
         List<WebReference> webReferenceList = webReferenceDAO.listPages();
         for (WebReference webReference : webReferenceList) {
            List<ConditionData> dataList = trainingData.get(webReference.getId());
            if (dataList == null || dataList.size() == 0) {
               continue;
            }
            Rule rule = new Rule();
            String className = systemConfiguration.getClasses().get(webReference.getType());
            if (className == null || className.equals("")) {
               className = String.valueOf(webReference.getType());
            }
            rule.setClassifiedClass(className);
            rule.setRuleType(Rule.BASE_RULE);
            session.save(rule);
            for (ConditionData data : dataList) {
               Condition condition = new Condition();
               condition.setWord(data.getWord());
               condition.setFuzzyAmount(data.getFuzzyAmount());
               condition.setRule(rule);
               session.save(condition);
            }
         }

         //Remove all unique conditions
         Query query = session.createQuery("SELECT c.word AS w, count(c.id) AS num FROM Condition AS c GROUP BY c.word");
         List result = query.list();
         for (Object obj : result) {
            Object[] valueList = (Object[]) obj;
            int count = Integer.parseInt(String.valueOf(valueList[1]));
            if (count == 1) {
               query = session.createQuery("DELETE FROM Condition AS c WHERE c.word = :value");
               query.setParameter("value", String.valueOf(valueList[0]));
               query.executeUpdate();
            }
         }

         tx.commit();
      } catch (Exception e) {
         LOG.error(e.getMessage());
         e.printStackTrace();
         if (tx != null) {
            tx.rollback();
         }
      } finally {
         session.close();
      }
   }

   @Override
   public int generateInitialRules() {
      List<RuleData> result = new LinkedList<RuleData>();
      systemConfiguration = specialQuery.getSystemConfiguration();
      int ruleLengthMaximum = systemConfiguration.getRuleLengthMaximum();
      int ruleLengthMinimum = systemConfiguration.getRuleLengthMinimum();
      allBaseRulesData = getAllBaseRulesData();
      initialRuleSet.clear();
      initialRuleSetSize = 0;

      for (RuleData baseRule : allBaseRulesData) {
         LOG.info("Generating initial rules from " + baseRule.toString());
         List<RuleData> sameClass = initialRuleSet.get(baseRule.getClassifiedClass());
         if (sameClass == null) {
            sameClass = new LinkedList<RuleData>();
         }
         List<RuleData> partialSet = generateInitialRulesFrom(baseRule, ruleLengthMinimum, ruleLengthMaximum);
         initialRuleSetSize += partialSet.size();
         sameClass.addAll(partialSet);
         initialRuleSet.put(baseRule.getClassifiedClass(), sameClass);
      }
      return initialRuleSetSize;
   }

   @Override
   public void removeAllBaseRules() {
      ruleDAO.removeAllBaseRules();
   }

   @Override
   public void removeAllClassifyRules() {
      ruleDAO.removeAllClassifyRules();
   }

   private List<RuleData> generateInitialRulesFrom(RuleData baseRule, int ruleLengthMinimum, int ruleLengthMaximum) {
      List<RuleData> result = new LinkedList<RuleData>();
      int ruleLength = baseRule.getConditions().size();
      int maxLength = ruleLength > ruleLengthMaximum ? ruleLengthMaximum : ruleLength;
      int minLength = ruleLength > ruleLengthMinimum ? ruleLengthMinimum : ruleLength;

      for (int i = minLength; i <= maxLength; i++) {
         LOG.info("Generate rules with length " + i + " for " + baseRule.toString());
         //SubSetGenerator combiner = new CombinationGeneratorImpl(ruleLength, i);
         SubSetGenerator combiner = new ReducedCombinationGeneratorImpl(ruleLength, i, 2000);
         while (combiner.hasMore()) {
            RuleData rule = new RuleData();
            rule.setClassifiedClass(baseRule.getClassifiedClass());
            int[] positions = combiner.getNext();
            for (int pos : positions) {
               rule.getConditions().add(baseRule.getConditions().get(pos));
            }
            result.add(rule);
         }
      }
      return result;
   }

   @Override
   public double getFitness(Chromosome individual) {
      if (ruleSet == null || ruleSet.size() == 0) {
         throw new IllegalStateException("Rule set must be specified first");
      }
      if (systemConfiguration == null) {
         systemConfiguration = specialQuery.getSystemConfiguration();
      }

      if (allBaseRulesData == null) {
         allBaseRulesData = getAllBaseRulesData();
      }

      int totalLength = 0;
      for (Long position : individual.getGenes()) {
         if (position < 0) {
            continue;
         }
         RuleData rule = ruleSet.get(position.intValue());
         totalLength += rule.getConditions().size();
      }

      int classifiedSampleCount = 0;
      for (RuleData sample : allBaseRulesData) {
         double maxWeight = 0;
         double winnerCount = 0;

         for (Long position : individual.getGenes()) {
            if (position < 0) {
               continue;
            }
            RuleData rule = ruleSet.get(position.intValue());
            double ruleWeight = getCompatibilityGrade(rule, sample.getConditions()) * getRuleWeight(rule);
            if (ruleWeight > maxWeight) {
               maxWeight = ruleWeight;
               winnerCount = 1;
            } else if (ruleWeight == maxWeight) {
               winnerCount++;
            }
         }

         if (winnerCount == 1) {
            classifiedSampleCount++;
         }
      }
      return systemConfiguration.getFitnessArgument1() * individual.getGenes().size() / totalLength +
              systemConfiguration.getFitnessArgument2() * classifiedSampleCount;
   }

   @Override
   public double getRuleWeight(RuleData ruleData) {
      return getConfidenceDegreeOf(ruleData);
   }

   @Override
   public void addManualRule(List<ConditionData> conds, String classifiedClass) {
      Session session = sessionFactory.openSession();
      Transaction tx = null;
      try {
         tx = session.beginTransaction();

         Rule rule = new Rule();
         rule.setRuleType(Rule.MANUAL_RULE);
         rule.setClassifiedClass(classifiedClass);
         session.save(rule);

         for (ConditionData conditionData : conds) {
            Condition condition = new Condition();
            condition.setRule(rule);
            condition.setWord(conditionData.getWord());
            condition.setFuzzyAmount(conditionData.getFuzzyAmount());
            session.save(condition);
         }

         tx.commit();
      } catch (Exception e) {
         if (tx != null) {
            tx.rollback();
         }
      } finally {
         session.close();
      }
   }

   @Override
   public int getSizeOfClassifyRules(boolean includeManualRules) {
      return ruleDAO.getSizeOfClassifyRules(includeManualRules);
   }

   public double getConfidenceDegreeOf(RuleData rule) {
      Double result = confidenceDegrees.get(rule);
      if (result != null) {
         return result;
      }
      return calculateConfidenceAndSupport(rule)[0];
   }

   public double getSupportDegreeOf(RuleData rule) {
      Double result = supportDegrees.get(rule);
      if (result != null) {
         return result;
      }
      return calculateConfidenceAndSupport(rule)[1];
   }

   @Override
   public double getCompatibilityGrade(RuleData rule, List<ConditionData> sample) {

      HashMap<String, String> ruleCondition = new HashMap<String, String>();
      for (ConditionData cond : rule.getConditions()) {
         ruleCondition.put(cond.getWord(), cond.getFuzzyAmount());
      }

      double ruleCompatibility = 0;
      for (ConditionData condData : sample) {
         if (ruleCondition.containsKey(condData.getWord())) {
            if (condData.getFuzzyAmount().equals("don't care")) {
               continue;
            }
//            if (ruleCompatibility == 0) {
//               ruleCompatibility = 1;
//            }
            double sampleFuzzyAmount = hedge2Quantifier.getQuantifyValue(condData.getFuzzyAmount());
            ruleCompatibility += getMembershipFunction(ruleCondition.get(condData.getWord())).getValueOf(sampleFuzzyAmount);
         }
      }
      return ruleCompatibility;
   }

   private MembershipFunction getMembershipFunction(String fuzzyAmount) {
      MembershipFunction result = cachedMembershipFunctions.get(fuzzyAmount);
      if (result == null) {
         result = new TriangleMembershipFunctionImpl();
         double middlePoint = hedge2Quantifier.getQuantifyValue(fuzzyAmount);
         double leftPoint = middlePoint - leftSideAdjustment;
         double rightPoint = middlePoint + rightSideAdjustment;
         if (leftPoint < 0) {
            leftPoint = 0;
         }
         if (rightPoint > 1) {
            rightPoint = 1;
         }
         result.setParameters(new double[]{leftPoint, middlePoint, rightPoint});
      }
      return result;
   }

   public Double[] calculateConfidenceAndSupport(RuleData rule) {

      LOG.debug("Calculating confidence and support for rule: " + rule.toString());

      if (allBaseRulesData == null) {
         allBaseRulesData = getAllBaseRulesData();
      }

      double totalFuzziness = 0.0;
      double totalClassFuzziness = 0.0;
      for (RuleData sample : allBaseRulesData) {
         double ruleFuzziness = getCompatibilityGrade(rule, sample.getConditions());
         if (ruleFuzziness != 1) {
            totalFuzziness += ruleFuzziness;
            if (sample.getClassifiedClass().equals(rule.getClassifiedClass())) {
               totalClassFuzziness += ruleFuzziness;
            }
         }
      }

      double confidence = totalFuzziness != 0.0 ? totalClassFuzziness / totalFuzziness : 0.0;
      confidenceDegrees.put(rule, confidence);
      double support = totalClassFuzziness / allBaseRulesData.size();
      supportDegrees.put(rule, support);
      return new Double[]{confidence, support};
   }

   private class RuleComparator implements Comparator<RuleData> {

      @Override
      public int compare(RuleData arg0, RuleData arg1) {
         if (arg0 == null) {
            if (arg1 != null) {
               return -1;
            } else {
               return 0;
            }
         } else {
            if (arg1 != null) {
               double value0 = getFlooringValue(arg0);
               double value1 = getFlooringValue(arg1);
               return value0 > value1 ? 1 : value0 < value1 ? -1 : 0;
            } else {
               return 1;
            }
         }
      }
   }

   private double getFlooringValue(RuleData arg0) {
      return getConfidenceDegreeOf(arg0) * getSupportDegreeOf(arg0);
   }


   @Override
   public List<RuleData> applyFlooringToInitialRules() {
      LOG.info("Start applying flooring method to initial rules.");
      SystemConfiguration systemConfiguration = specialQuery.getSystemConfiguration();
      int maxRuleCount = systemConfiguration.getFlooringSize();

      RuleComparator ruleComparator = new RuleComparator();

      List<RuleData> result = new LinkedList<RuleData>();
      for (String classifiedClass : initialRuleSet.keySet()) {
         List<RuleData> classifiedClassRuleSet = initialRuleSet.get(classifiedClass);
         int acceptedCount = classifiedClassRuleSet.size() * maxRuleCount / initialRuleSetSize;
         int expectedSize = acceptedCount > classifiedClassRuleSet.size() ? classifiedClassRuleSet.size() : acceptedCount;
         TreeSet<RuleData> sortedList = new TreeSet<RuleData>(ruleComparator);
         for (RuleData rule : classifiedClassRuleSet) {
            if (sortedList.size() < expectedSize) {
               sortedList.add(rule);
            } else {
               RuleData lowest = sortedList.first();
               int order = ruleComparator.compare(rule, lowest);
               if (order > 0) {
                  RuleData removed = sortedList.pollFirst();
                  LOG.debug("Floor to rule " + removed);
                  sortedList.add(rule);
               }
            }
         }
         result.addAll(sortedList);
      }
      return result;
   }

   public void setRuleSet(List<RuleData> ruleSet) {
      this.ruleSet = ruleSet;
   }

   @Override
   public void afterPropertiesSet() throws Exception {
      SystemConfiguration systemConfiguration = specialQuery.getSystemConfiguration();
      leftSideAdjustment = systemConfiguration.getTriangleBottomLeft();
      rightSideAdjustment = systemConfiguration.getTriangleBottomRight();
      specialQuery.addCallbackAction(Constants.UPDATE_RULE_SERVICE, new Runnable() {
         @Override
         public void run() {
            SystemConfiguration systemConfiguration = specialQuery.getSystemConfiguration();
            leftSideAdjustment = systemConfiguration.getTriangleBottomLeft();
            rightSideAdjustment = systemConfiguration.getTriangleBottomRight();
         }
      });
      LOG.info("Prefetching rule weight...");
      List<RuleData> classifyRules = getClassifyRuleSet(systemConfiguration.isIncludeManualRules());
      for (RuleData rule : classifyRules) {
         calculateConfidenceAndSupport(rule);
      }
      LOG.info("Prefetching done.");
   }


   @Override
   public List<RuleData> generateClassifyRuleSet(List<RuleData> flooredRuleSet) {
      LOG.info("Start generating classification rule set.");
      ruleSet = flooredRuleSet;
      SystemConfiguration systemConfig = specialQuery.getSystemConfiguration();
      int maxRuleCount = systemConfig.getClassifiedRuleSetSize();
      List<Chromosome> initialPopulation = new ArrayList<Chromosome>();
      for (int i = 0; i < systemConfig.getInitialPopulationSize(); i++) {
         initialPopulation.add(getAChromosome(flooredRuleSet, maxRuleCount));
      }
      geneticAlgorithm.setGenerationLimit(systemConfig.getGenerationMaximum());
      geneticAlgorithm.setInitialPopulation(initialPopulation);
      Chromosome bestIndividual = geneticAlgorithm.getFitnessChromosome();
      List<RuleData> result = new ArrayList<RuleData>();
      for (Long position : bestIndividual.getGenes()) {
         if (position < 0) {
            continue;
         }
         RuleData gene = flooredRuleSet.get(position.intValue());
         result.add(gene);
      }
      return result;
   }

   @Override
   public void persistClassifyRuleSet(List<RuleData> classifyRuleSet) {
      Session session = sessionFactory.openSession();
      Transaction tx = null;
      try {
         tx = session.beginTransaction();

         for (RuleData ruleData : classifyRuleSet) {
            Rule rule = new Rule();
            rule.setRuleType(Rule.CLASSIFY_RULE);
            rule.setClassifiedClass(ruleData.getClassifiedClass());
            session.save(rule);

            for (ConditionData conditionData : ruleData.getConditions()) {
               Condition condition = new Condition();
               condition.setRule(rule);
               condition.setWord(conditionData.getWord());
               condition.setFuzzyAmount(conditionData.getFuzzyAmount());
               session.save(condition);
            }
         }

         tx.commit();
      } catch (Exception e) {
         if (tx != null) {
            tx.rollback();
         }
      } finally {
         session.close();
      }
   }

   @Override
   public List<RuleData> getClassifyRuleSet(boolean includeManualRules) {
      List<Rule> classifyRules = ruleDAO.findAllClassifyRules(includeManualRules);
      List<RuleData> result = new LinkedList<RuleData>();
      for (Object obj : classifyRules) {
         Rule rule = (Rule) obj;
         RuleData ruleData = new RuleData();
         ruleData.setClassifiedClass(rule.getClassifiedClass());

         @SuppressWarnings("rawtypes")
         List conditions = conditionDAO.findAllByRuleId(rule.getId());

         for (Object obj2 : conditions) {
            Condition condition = (Condition) obj2;
            ConditionData conditionData = new ConditionData();
            conditionData.setFuzzyAmount(condition.getFuzzyAmount());
            conditionData.setWord(condition.getWord());
            ruleData.getConditions().add(conditionData);
         }
         result.add(ruleData);
      }
      return result;
   }

   Chromosome getAChromosome(List<RuleData> flooredRuleSet, int maxRuleCount) {
      int flooredRuleSetSize = flooredRuleSet.size();
      int maxNumRuleForEachClass = maxRuleCount / initialRuleSet.size();
      int actualRuleCount = maxNumRuleForEachClass * initialRuleSet.size();
      Map<String, Integer> classifiedClassRuleCount = new HashMap<String, Integer>();
      Chromosome result = new Chromosome();
      for (int i = 0; i < actualRuleCount; i++) {
         boolean gotRule = false;
         while (!gotRule) {
            int position = (int) (Math.random() * (flooredRuleSetSize - 1));
            LOG.debug(position);
            if (position >= 0) {
               RuleData selectedRule = flooredRuleSet.get(position);
               Integer ruleCount = classifiedClassRuleCount.get(selectedRule.getClassifiedClass());
               if (ruleCount != null && ruleCount >= maxNumRuleForEachClass) {
                  continue;
               }
               if (ruleCount == null) {
                  ruleCount = 0;
               }
               ruleCount++;
               classifiedClassRuleCount.put(selectedRule.getClassifiedClass(), ruleCount);
            }

            result.getGenes().add(Long.valueOf(position));
            gotRule = true;
            LOG.debug("got Rule");
         }
      }
      LOG.info("Got a Chromosome: " + result.toString());
      return result;
   }

}
